#include "glUtils.h"

#include <assert.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

GLuint LoadShader(GLenum type, const char *shaderSrc) {
    GLuint shader;
    GLint compiled;
    // Create the shader object
    shader = glCreateShader(type);
    if (shader == 0)
        return 0;
    // Load the shader source
    glShaderSource(shader, 1, (const GLchar *const *)&shaderSrc, NULL);
    // Compile the shader
    glCompileShader(shader);
    // Check the compile status
    glGetShaderiv(shader, GL_COMPILE_STATUS, &compiled);

    if (!compiled) {
        GLint infoLen = 0;

        glGetShaderiv(shader, GL_INFO_LOG_LENGTH, &infoLen);

        if (infoLen > 1) {
            char *infoLog = malloc(sizeof(char) * infoLen);

            glGetShaderInfoLog(shader, infoLen, NULL, infoLog);
            printf("Error compiling shader:\n%s\n", infoLog);

            free(infoLog);
        }
        glDeleteShader(shader);
        return 0;
    }
    return shader;
}

int LoadShaderFile(GLenum type, const char *file) {
    FILE *shaderFile;
    char *buffer;
    shaderFile = fopen(file, "r");
    assert(shaderFile != NULL);
    fseek(shaderFile, 0, SEEK_END);
    size_t fileSize = ftell(shaderFile);
    rewind(shaderFile);
    buffer = (char *)malloc(fileSize);
    memset(buffer, 0, fileSize);
    fread(buffer, 1, fileSize, shaderFile);
    fclose(shaderFile);
    GLuint shader = LoadShader(type, buffer);
    free(buffer);
    return shader;
}

GLuint LoadShaderProgram(const char *shaderName) {
    GLuint vertexShader;
    GLuint fragmentShader;
    GLuint programObject;
    char fileName[128];
    sprintf(fileName, "shader/%s.vert", shaderName);
    vertexShader = LoadShaderFile(GL_VERTEX_SHADER, fileName);
    sprintf(fileName, "shader/%s.frag", shaderName);
    fragmentShader = LoadShaderFile(GL_FRAGMENT_SHADER, fileName);
    programObject = glCreateProgram();
    if (programObject == 0)
        return 0;
    glAttachShader(programObject, vertexShader);
    glAttachShader(programObject, fragmentShader);
    return programObject;
}

GLuint InitShader(const char *shaderSrc) {
    GLuint program = LoadShaderProgram(shaderSrc);
    if (program == 0) {
        return 0;
    }
    glLinkProgram(program);
    GLint linked;
    glGetProgramiv(program, GL_LINK_STATUS, &linked);
    if (!linked) {
        GLint infoLen = 0;
        glGetProgramiv(program, GL_INFO_LOG_LENGTH, &infoLen);
        if (infoLen > 1) {
            char *infoLog = (char *)malloc(sizeof(char) * infoLen);

            glGetProgramInfoLog(program, infoLen, NULL, infoLog);
            printf("Error linking program:\n%s\n", infoLog);
            free(infoLog);
        }
        glDeleteProgram(program);
        return 0;
    }
    return program;
}

GLuint InitArrayBuffer(float *data, size_t size) {
    GLuint buff;
    glGenBuffers(1, &buff);
    assert(buff != 0);
    glBindBuffer(GL_ARRAY_BUFFER, buff);
    glBufferData(GL_ARRAY_BUFFER, size, data, GL_STATIC_DRAW);
    return buff;
}

void InitAttrBuffer(GLuint buffer, GLuint attr, size_t num) {
    glBindBuffer(GL_ARRAY_BUFFER, buffer);
    glVertexAttribPointer(attr, num, GL_FLOAT, GL_FALSE, 0, 0);
    glEnableVertexAttribArray(attr);
}

GLuint InitAttrArrayBuffer(GLuint program, float *data, size_t size, size_t num,
                           const char *attr) {
    GLuint buff;
    glGenBuffers(1, &buff);
    assert(buff != 0);
    glBindBuffer(GL_ARRAY_BUFFER, buff);
    glBufferData(GL_ARRAY_BUFFER, size, data, GL_STATIC_DRAW);
    GLint a_attr = glGetAttribLocation(program, attr);
    assert(a_attr >= 0);
    glVertexAttribPointer(a_attr, num, GL_FLOAT, GL_FALSE, 0, 0);
    glEnableVertexAttribArray(a_attr);
    glBindBuffer(GL_ARRAY_BUFFER, 0);
    return buff;
}

GLuint InitElementsBuffer(void *data, size_t size) {
    glBindBuffer(GL_ARRAY_BUFFER, 0);
    GLuint idx_buf;
    glGenBuffers(1, &idx_buf);
    if (!idx_buf) {
        return 0;
    }
    glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, idx_buf);
    glBufferData(GL_ELEMENT_ARRAY_BUFFER, size, data, GL_STATIC_DRAW);
    return idx_buf;
}

GLuint InitTexture(GLuint program, SDL_Surface *sur, uint32_t texn,
                   const char *uni) {
    GLint u_sam = glGetUniformLocation(program, uni);
    assert(u_sam >= 0);
    GLuint tex;
    glGenTextures(1, &tex);
    assert(tex != 0);
    glActiveTexture(GL_TEXTURE0 + texn);
    glBindTexture(GL_TEXTURE_2D, tex);
    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, sur->w, sur->h, 0, GL_RGB,
                 GL_UNSIGNED_BYTE, sur->pixels);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
    glUniform1i(u_sam, texn);
    return tex;
}

float axis_xyz[] = {0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0,
                    0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0};
float axis_color[] = {1.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0, 0.0,
                      0.0, 1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0};

axis_t InitAxis() {
    axis_t axis;
    axis.program = InitShader("axis");
    assert(axis.program != 0);

    axis.u_mvp = glGetUniformLocation(axis.program, "u_MvpMatrix");
    assert(axis.u_mvp >= 0);

    axis.a_xyz = glGetAttribLocation(axis.program, "axis_Position");
    assert(axis.a_xyz >= 0);
    axis.a_color = glGetAttribLocation(axis.program, "axis_Color");
    assert(axis.a_color >= 0);

    axis.buf_xyz = InitArrayBuffer(axis_xyz, sizeof(axis_xyz));
    assert(axis.buf_xyz != 0);
    axis.buf_color = InitArrayBuffer(axis_color, sizeof(axis_color));
    assert(axis.buf_color != 0);

    return axis;
}
const GLbyte axis_idx[] = {0, 1, 2, 3, 4, 5};

void DrawAxis(axis_t *axis, float *mvp) {
    glClear(GL_DEPTH_BUFFER_BIT);
    glUseProgram(axis->program);
    glUniformMatrix4fv(axis->u_mvp, 1, GL_FALSE, mvp);

    glBindBuffer(GL_ARRAY_BUFFER, axis->buf_xyz);
    glVertexAttribPointer(axis->a_xyz, 3, GL_FLOAT, GL_FALSE, 0, 0);
    glEnableVertexAttribArray(axis->a_xyz);

    glBindBuffer(GL_ARRAY_BUFFER, axis->buf_color);
    glVertexAttribPointer(axis->a_color, 3, GL_FLOAT, GL_FALSE, 0, 0);
    glEnableVertexAttribArray(axis->a_color);

    glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
    glDrawElements(GL_LINES, 6, GL_UNSIGNED_BYTE, axis_idx);
}

void DrawPoints(axis_t *axis, float *mvp, float *points, float* colors, size_t num) {
    // glClear(GL_DEPTH_BUFFER_BIT);
    glPointSize(10);

    glUseProgram(axis->program);
    glUniformMatrix4fv(axis->u_mvp, 1, GL_FALSE, mvp);
    glBindBuffer(GL_ARRAY_BUFFER, 0);
    glVertexAttribPointer(axis->a_xyz, 3, GL_FLOAT, GL_FALSE, 0, points);
    glEnableVertexAttribArray(axis->a_xyz);
    glVertexAttribPointer(axis->a_color, 3, GL_FLOAT, GL_FALSE, 0, colors);
    glEnableVertexAttribArray(axis->a_color);

    glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
    glDrawElements(GL_POINTS, num, GL_UNSIGNED_BYTE, axis_idx);
}